// ValidationAspects
// Written by Mike Saunders
// Released under Microsoft Public License (Ms-PL)
using System;
using System.Collections.Generic;
using System.Linq;
using ValidationAspects.Factories;
using ValidationAspects.Sdk;

namespace ValidationAspects.Core
{
	internal class ValidationRegistry<T> where T : class
	{
		#region Inner
		private class NullValidator : IValidator
		{
			#region Implementation of IValidator
			public void Invoke(object value, IValidationContext context) {}
			#endregion
		}
		#endregion

		private readonly ValidatorCache<T> cache = new ValidatorCache<T>();
		private readonly Func<T, IEnumerable<IValidatorFactory>> _discoverFactories;
		private readonly Func<T, Type> _selectValueType;
		private static readonly NullValidator nullValidator = new NullValidator();

		public ValidationRegistry(Func<T, Type> selectTypeFunc, Func<T, IEnumerable<IValidatorFactory>> factoryDiscoveryFunc)
		{
			Enforce.IsNotNull(selectTypeFunc, "selectTypeFunc");
			Enforce.IsNotNull(factoryDiscoveryFunc, "factoryDiscoveryFunc");

			_selectValueType = selectTypeFunc;
			_discoverFactories = factoryDiscoveryFunc;
		}

		public void ClearCache()
		{
			cache.Clear();
		}

		public IValidatorFactory[] GetFactories(T target)
		{
			Enforce.IsNotNull(target, "target");

			IValidatorFactory[] factories = cache.GetFactories(target);
			if (factories == null)
			{
				factories = _discoverFactories(target).ToArray();
				cache.SetFactories(target, factories);
			}

			return factories;
		}

		public void RevertValidation(T target)
		{
			Enforce.IsNotNull(target, "target");
			cache.Clear(target);

			// resolve to fail fast if validator factories are unable to create validator
			ResolveValidator(target);
		}

		public void ReplaceValidation(T target, IEnumerable<IValidatorFactory> factories)
		{
			Enforce.IsNotNull(target, "target");
			Enforce.IsNotNull(factories, "factories");

			cache.SetFactories(target, factories.ToArray());

			// resolve to fail fast if validator factories are unable to create validator
			ResolveValidator(target);
		}

		public void ReplaceValidation<TValue>(T target, Action<TValue, IValidationContext> validateAction)
		{
			Enforce.IsNotNull(validateAction, "validateAction");

			ReplaceValidation(target, new[] {new InlineValidatorFactory<TValue>(validateAction)});
		}

		public void AddValidation(T target, IEnumerable<IValidatorFactory> factories)
		{
			Enforce.IsNotNull(target, "target");
			Enforce.IsNotNull(factories, "factories");

			cache.SetFactories(target, factories.Concat(GetFactories(target)).ToArray());

			// resolve to fail fast if validator factories are unable to create validator
			ResolveValidator(target);
		}

		public void AddValidation<TValue>(T target, Action<TValue, IValidationContext> validateAction)
		{
			Enforce.IsNotNull(validateAction, "validateAction");

			AddValidation(target, new[] {new InlineValidatorFactory<TValue>(validateAction)});
		}

		public IValidator ResolveValidator(T target)
		{
			Enforce.IsNotNull(target, "target");

			// is validator cached?
			IValidator validator = cache.GetValidator(target);
			if (validator != null)
				return validator == nullValidator ? null : validator;

			// no build it
			try
			{
				IValidatorFactory[] factories = GetFactories(target);
				if (factories.Length == 0 || factories.Length == 1 && factories[0].GetType() == typeof(NoValidation)) // add validator to cache so no need to re-discover for every request
				{
					validator = nullValidator;
					cache.SetValidator(target, validator);
					return null;
				}

				validator = factories.Length == 1 ? factories[0].Create(_selectValueType(target)) : new CompoundValidatorFactory(factories).Create(_selectValueType(target));
				cache.SetValidator(target, validator);

				return validator;
			}
			catch (Exception e)
			{
				throw new ValidationException(string.Format("Failed to construct validator for [type:{0}].", _selectValueType(target)), e);
			}
		}
	}
}